using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;

namespace Magiq.Support {
  public class ReflectionBuilder {
    private static readonly Dictionary<Type, OpCode> valueTypesOpCodes;

    static ReflectionBuilder() {
      valueTypesOpCodes = new Dictionary<Type, OpCode>
                            {
                              {typeof (sbyte), OpCodes.Ldind_I1},
                              {typeof (byte), OpCodes.Ldind_U1},
                              {typeof (char), OpCodes.Ldind_U2},
                              {typeof (short), OpCodes.Ldind_I2},
                              {typeof (ushort), OpCodes.Ldind_U2},
                              {typeof (int), OpCodes.Ldind_I4},
                              {typeof (uint), OpCodes.Ldind_U4},
                              {typeof (long), OpCodes.Ldind_I8},
                              {typeof (ulong), OpCodes.Ldind_I8},
                              {typeof (bool), OpCodes.Ldind_I1},
                              {typeof (double), OpCodes.Ldind_R8},
                              {typeof (float), OpCodes.Ldind_R4}
                            };
    }

    public Action<object, object> PropertySetter(Type type, string property) {
      var m = new DynamicMethod("Set" + property,
                                typeof (void), new[] {typeof (object), typeof (object)},
                                type, true);

      var info = type.GetProperty(property);

      var ilgen = m.GetILGenerator();
      ilgen.DeclareLocal(info.PropertyType);
      ilgen.Emit(OpCodes.Ldarg_0); //Load the first argument 

      //(target object)

      //Cast to the source type

      ilgen.Emit(OpCodes.Castclass, type);
      ilgen.Emit(OpCodes.Ldarg_1); //Load the second argument 

      //(value object)

      if (info.PropertyType.IsValueType) {
        ilgen.Emit(OpCodes.Unbox, info.PropertyType); //Unbox it 

        if (valueTypesOpCodes.ContainsKey(info.PropertyType)) //and load
        {
          var load = valueTypesOpCodes[info.PropertyType];
          ilgen.Emit(load);
        }
        else {
          ilgen.Emit(OpCodes.Ldobj, info.PropertyType);
        }
      }
      else {
        ilgen.Emit(OpCodes.Castclass, info.PropertyType); //Cast class
      }

      var set = info.GetSetMethod(true);
      if (set.IsVirtual)
        ilgen.Emit(OpCodes.Callvirt, set);
      else
        ilgen.Emit(OpCodes.Call, set);

      //return
      ilgen.Emit(OpCodes.Ret);


      return (Action<object, object>) m.CreateDelegate(typeof (Action<object, object>));
    }

    public Func<object, object> FieldGetter(Type type, string field) {
      var d = new DynamicMethod("Get" + field, typeof (object), new[] {typeof (object)}, type, true);

      var info = type.GetField(field, BindingFlags.Instance | BindingFlags.NonPublic);
      // Get the IL generator for the method
      var ilgen = d.GetILGenerator();


      // load obj
      ilgen.Emit(OpCodes.Ldarg_0);

      // loads the value of the field
      ilgen.Emit(OpCodes.Ldfld, info);

      if (info.FieldType.IsValueType)
        ilgen.Emit(OpCodes.Box, info.FieldType); //Box it
      else
        ilgen.Emit(OpCodes.Castclass, typeof (object)); //Cast class

      //return
      ilgen.Emit(OpCodes.Ret);

      return (Func<object, object>) d.CreateDelegate(typeof (Func<object, object>));
    }

    public Func<object, object> PropertyGetter(Type type, string property) {
      var d = new DynamicMethod("Get" + property, typeof (object), new[] {typeof (object)}, type, true);

      var info = type.GetProperty(property);

      var getter = info.GetGetMethod(true);

      // Get the IL generator for the method
      var ilgen = d.GetILGenerator();

      // Declare p (loc0)
      ilgen.DeclareLocal(type);

      // load obj
      ilgen.Emit(OpCodes.Ldarg_0);
      // cast obj
      ilgen.Emit(OpCodes.Castclass, type);
      // stores the result in p
      ilgen.Emit(OpCodes.Stloc_0);

      // load p
      ilgen.Emit(OpCodes.Ldloc_0);
      // loads the value of the property
      ilgen.EmitCall(OpCodes.Call, getter, null);

      if (info.PropertyType.IsValueType)
        ilgen.Emit(OpCodes.Box, info.PropertyType); //Box it
      else
        ilgen.Emit(OpCodes.Castclass, typeof (object)); //Cast class

      //return
      ilgen.Emit(OpCodes.Ret);

      return (Func<object, object>) d.CreateDelegate(typeof (Func<object, object>));
    }
  }
}